﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace LogitechLCDWrapper
{
    public abstract class LCDScreen : IDisposable
    {

        #region DllImports
        protected const string DllPath = "LogitechLcd.dll";
        [DllImport(DllPath, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
        static extern bool LogiLcdInit(StringBuilder title, int lcdType);

        [DllImport(DllPath, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
        static extern void LogiLcdShutdown();

        [DllImport(DllPath, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
        protected static extern bool LogiLcdIsConnected(int lcdType);

        [DllImport(DllPath, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
        static extern void LogiLcdUpdate();

        [DllImport(DllPath, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
        static extern bool LogiLcdIsButtonPressed(int button);
        #endregion

        #region Fields
        /// <summary>
        /// Used to synchronize usage of bitmap for drawing
        /// </summary>
        private object syncRoot = new object();
        /// <summary>
        /// used to determine if device is disposed
        /// </summary>
        private bool _disposed;
        /// <summary>
        /// used to determine if I should send notification that no buttons are pressed after a button is pressed
        /// </summary>
        private bool warnedNoButtonPressed = false;
        /// <summary>
        /// determine the previous refresh time to know if I should refresh the screen or not
        /// </summary>
        private DateTime previousRefresh;
        /// <summary>
        /// Determine the previous pressed state buttons
        /// </summary>
        private LCDButtons previousPressedState;
        /// <summary>
        /// Used to know wich buttons are monitored but the Click and Pressed events
        /// </summary>
        private List<LCDButtons> _buttons;
        /// <summary>
        /// Task that refresh the screen
        /// </summary>
        private Task AlwaysRunningTask;
        /// <summary>
        /// CancellationToken used to cancel the refresh task when disposing the object
        /// </summary>
        private System.Threading.CancellationTokenSource cts;
        /// <summary>
        /// Used to post event on the right thread
        /// </summary>
        private System.Threading.SynchronizationContext callingSynchronizationContext;
        /// <summary>
        /// sed as buffer to display on the screen
        /// </summary>
        private System.Drawing.Bitmap _background;
        /// <summary>
        /// Used to access to advanced drawing functions
        /// </summary>
        private System.Drawing.Graphics _graphicsDevice;
        /// <summary>
        /// Used to know if BeginDrawing is called before using GraphicDevice to ensure Thread safety
        /// </summary>
        private bool isDrawing;
        /// <summary>
        /// Get the list of LCDButtons monitored for Click en Pressed events.
        /// By default, all buttons are monitored
        /// </summary>
        protected virtual List<LCDButtons> Buttons
        {
            get
            {
                if (_buttons == null)
                {
                    _buttons = new List<LCDButtons>((LCDButtons[])Enum.GetValues(typeof(LCDButtons)));
                }
                return _buttons;
            }
        }
        protected System.Drawing.Bitmap Background
        {
            get { return _background; }
            set
            {
                BeginDrawing();
                {
                    _background = value.Clone(new Rectangle(0, 0, ScreenWidth > value.Width ? value.Width : ScreenWidth, ScreenHeight > value.Height ? value.Height : ScreenHeight), System.Drawing.Imaging.PixelFormat.Format32bppArgb);
                }
                EndDrawing();
            }
        }
        /// <summary>
        /// Lines of text on the screen
        /// </summary>
        protected string[] Lines;
        /// <summary>
        /// Scroll time when text is too long
        /// </summary>
        protected DateTime[] previousScrollTime;
        /// <summary>
        /// Scroll position
        /// </summary>
        protected int[] scrollIndices;
        /// <summary>
        /// Determine if we scroll to the left r to the right
        /// </summary>
        protected int[] scrollDirection;


        #endregion

        #region Properties
        /// <summary>
        /// Max number of lines on the screen
        /// </summary>
        public int MaxLines { get; protected set; }
        /// <summary>
        /// Max number of char displayed on a line
        /// </summary>
        public int MaxCharPerLine { get; protected set; }
        /// <summary>
        /// Screen width in pixels
        /// </summary>
        public int ScreenWidth { get; protected set; }
        /// <summary>
        /// Screen height in pixels
        /// </summary>
        public int ScreenHeight { get; protected set; }
        /// <summary>
        /// Background size of the screen
        /// </summary>
        public int BackgroundSize { get; protected set; }
        /// <summary>
        /// Refresh frequency on the screen in Hz
        /// </summary>
        public int RefreshFrequency { get; protected set; }

        /// <summary>
        /// Screen size in pixels
        /// </summary>
        public virtual int ScreenSize
        {
            get
            {
                return ScreenHeight * ScreenWidth;
            }
        }
        /// <summary>
        /// Get if the screen is initialized
        /// </summary>
        public bool Initialized { get; private set; }
        /// <summary>
        /// Enable or disable text scrolling
        /// </summary>
        public bool EnableTextScrolling { get; set; }
        /// <summary>
        /// Scroll speed when text is too long
        /// </summary>
        public double ScrollSpeed { get; set; }
        /// <summary>
        /// Title of the applet
        /// </summary>
        public string Title { get; set; }
        /// <summary>
        /// Property to use to draw on the screen
        /// </summary>
        public System.Drawing.Graphics GraphicDevice
        {
            get
            {
                if (isDrawing)
                {
                    return _graphicsDevice;
                }
                else
                {
                    throw new InvalidOperationException("You can't use GraphicsDevice before calling BeginDrawing");
                }
            }
        }
        #endregion

        #region Abstract
        /// <summary>
        /// Initialize screen with values like ScreenHeight, ScreenWidth, etc...
        /// </summary>
        protected abstract void Initialize();
        /// <summary>
        /// Custom method to draw image on the screen
        /// </summary>
        /// <param name="elapsedTime">Time between call of refresh</param>
        protected abstract void DrawBackground(TimeSpan elapsedTime);
        /// <summary>
        /// Used to draw text on line
        /// </summary>
        /// <param name="elapsedTime">Time between call of refresh</param>
        protected abstract void DrawText(TimeSpan elapsedTime);

        /// <summary>
        /// Used to know if screen is connected or not
        /// </summary>
        public abstract bool IsConnected { get; }
        

        #endregion

        #region Events
        /// <summary>
        /// Raised when Button is down
        /// </summary>
        public event EventHandler<LCDButtonsDownEventArgs> LCDButtonDown;
        /// <summary>
        /// Raised when Button is clicked
        /// </summary>
        public event EventHandler<LCDButtonsClickedEventArgs> ButtonClicked;
        /// <summary>
        /// Raise event LCDButonClicked
        /// </summary>
        /// <param name="btnClicked"></param>
        private void OnLCDButtonClicked(object btnClicked)
        {
            if (ButtonClicked != null)
            {
                ButtonClicked(this, new LCDButtonsClickedEventArgs((LCDButtons)btnClicked));
            }
        }
        /// <summary>
        /// Raise event LCDButtonDown
        /// </summary>
        /// <param name="btnPressed"></param>
        private void OnLCDButtonsDown(object btnPressed)
        {
            if (LCDButtonDown != null)
            {
                LCDButtonDown(this, new LCDButtonsDownEventArgs((LCDButtons)btnPressed));
            }
        }
        /// <summary>
        /// used to determine if a button is pressed
        /// </summary>
        /// <param name="buttons">Button to check</param>
        /// <returns></returns>
        private bool IsButtonDown(LCDButtons buttons)
        {
            ThrowIfNotInitializedOrDisposed();
            return LogiLcdIsButtonPressed((int)buttons);
        }
        #endregion
     

        /// <summary>
        /// Initialize instance of base LCDScreen
        /// </summary>
        /// <param name="appTitle">Title of your app</param>
        /// <param name="type">Type of LCS Screen</param>
        public LCDScreen(string appTitle, LCDType type)
        {

            StringBuilder builder = new StringBuilder(appTitle);
            this.Initialized = LogiLcdInit(builder, (int)type);

            //if initialization failed, throw exception
            ThrowIfNotInitializedOrDisposed();

            //initialize constants of LCD screen
            //this function must be overriden in child class
            Initialize();
            
            _background = new Bitmap(ScreenWidth, ScreenHeight);
            Title = appTitle;
            Lines = new string[MaxLines];
            scrollIndices = new int[MaxLines];
            previousScrollTime = new DateTime[MaxLines];
            scrollDirection = new int[MaxLines];
            callingSynchronizationContext = System.Threading.SynchronizationContext.Current;
            previousPressedState = LCDButtons.None;

            for (int i = 0; i < MaxLines; i++)
            {
                Lines[i] = "";
                previousScrollTime[i] = DateTime.Now;
                scrollDirection[i] = 1;
            }
            cts = new System.Threading.CancellationTokenSource();
            previousRefresh = DateTime.Now;
            ScrollSpeed = 1;
            _graphicsDevice = System.Drawing.Graphics.FromImage(_background);
            AlwaysRunningTask = new Task(() => { while (!cts.Token.IsCancellationRequested) { RefreshScreen(); } }, cts.Token);
            AlwaysRunningTask.Start();
        }
        /// <summary>
        /// Clear LCD screen background
        /// </summary>
        public void ClearBackground()
        {
            BeginDrawing();
            for (int y = 0; y < ScreenHeight; y++)
            {
                for (int x = 0; x < ScreenWidth; x++)
                {
                    Background.SetPixel(x, y, Color.Transparent);
                }
            }
            EndDrawing();
        }
        /// <summary>
        /// Indicate to the wrapper that you want to draw something on the screen
        /// </summary>
        public void BeginDrawing()
        {
            System.Threading.Monitor.Enter(syncRoot);
            isDrawing = true;
        }
        /// <summary>
        /// Indicate to the wrapper that you have finished drawing
        /// </summary>
        public void EndDrawing()
        {
            System.Threading.Monitor.Exit(syncRoot);
            isDrawing = false;
        }

        private void RefreshScreen()
        {
            TimeSpan elapsedTime = DateTime.Now.Subtract(previousRefresh);
            //refresh screen at 60 Hz, useless to be quicker as LCD screen is limited
            if (elapsedTime.Ticks > (TimeSpan.TicksPerSecond / RefreshFrequency))
            {
                CheckIfButtonPressed();
                BeginDrawing();
                {

                    DrawBackground(elapsedTime);
                    DrawText(elapsedTime);
                    LogiLcdUpdate();
                    previousRefresh = DateTime.Now;
                }
                EndDrawing();
            }
        }
        private void CheckIfButtonPressed()
        {
            LCDButtons btnPressed = LCDButtons.None;
            foreach (LCDButtons btn in Buttons)
            {
                btnPressed |= IsButtonDown(btn) ? btn : LCDButtons.None;
                //check if previous state = 1 and current state = 0 : then this button is clicked
                if ((previousPressedState & btn) == btn && (btnPressed & btn) != btn)
                {
                    callingSynchronizationContext.Post(new System.Threading.SendOrPostCallback(OnLCDButtonClicked), btn);
                }
            }
            previousPressedState = btnPressed;

            if (btnPressed != LCDButtons.None)
            {
                callingSynchronizationContext.Post(new System.Threading.SendOrPostCallback(OnLCDButtonsDown), btnPressed);
                warnedNoButtonPressed = false;
            }
            //ensure that we indicate only once that no button is pressed
            if (btnPressed == LCDButtons.None && !warnedNoButtonPressed)
            {
                callingSynchronizationContext.Post(new System.Threading.SendOrPostCallback(OnLCDButtonsDown), LCDButtons.None);
                warnedNoButtonPressed = true;
            }
        }
        private void ThrowIfNotInitializedOrDisposed()
        {
            if (!Initialized || _disposed)
            {
                throw new NotInitializedException();
            }
        }

        #region IDisposable Membres & destructor
        ~LCDScreen()
        {
            if (!_disposed && Initialized)
                Dispose();
        }

        public void Dispose()
        {
            if (!_disposed && Initialized)
            {
                cts.Cancel();
                AlwaysRunningTask.Wait();
                LogiLcdShutdown();
                _disposed = true;
            }
        }

        #endregion
    }
}
